// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.Text.RegularExpressions;
using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;

public class MergeConfigurationProperties : Task
{
    [Required]
    public ITaskItem[] CandidateConfigurations { get; set; }

    [Required]
    public ITaskItem[] ProjectReferences { get; set; }

    [Output]
    public ITaskItem[] ProjectConfigurations { get; set; }

    public override bool Execute()
    {
        try
        {
            ProjectConfigurations = new ITaskItem[CandidateConfigurations.Length];

            for (var i = 0; i < CandidateConfigurations.Length; i++)
            {
                var configuration = CandidateConfigurations[i];
                var foundProjectReference = FindMatchingProject(configuration);
                if (foundProjectReference == null)
                {
                    Log.LogError(
                        "Unable to find a project reference for project configuration item '{0}'",
                        configuration.ItemSpec);

                    return false;
                }

                var entry = new TaskItem(configuration.ItemSpec, new Dictionary<string, string>
                {
                    ["Version"] = configuration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.Version)),
                    ["Source"] = configuration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.Source)),
                    ["GetBuildAssetsTargets"] = configuration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.GetBuildAssetsTargets)),
                    ["AdditionalBuildProperties"] = configuration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalBuildProperties)),
                    ["AdditionalBuildPropertiesToRemove"] = configuration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalBuildPropertiesToRemove)),
                    ["GetPublishAssetsTargets"] = configuration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.GetPublishAssetsTargets)),
                    ["AdditionalPublishProperties"] = configuration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalPublishProperties)),
                    ["AdditionalPublishPropertiesToRemove"] = configuration.GetMetadata(nameof(StaticWebAssetsManifest.ReferencedProjectConfiguration.AdditionalPublishPropertiesToRemove)),
                });

                var additionalBuildProperties = string.Join(
                    ";", configuration.GetMetadata("AdditionalBuildProperties"),
                    foundProjectReference.GetMetadata("SetConfiguration"),
                    foundProjectReference.GetMetadata("SetPlatform"),
                    foundProjectReference.GetMetadata("SetTargetFramework"));

                CleanupMetadata(entry, "AdditionalBuildProperties", additionalBuildProperties);

                var buildPropertiesToRemove = string.Join(
                    ";", configuration.GetMetadata("AdditionalBuildPropertiesToRemove"),
                    foundProjectReference.GetMetadata("GlobalPropertiesToRemove"),
                    foundProjectReference.GetMetadata("UndefineProperties"));

                CleanupMetadata(entry, "AdditionalBuildPropertiesToRemove", buildPropertiesToRemove);

                var additionalPublishProperties = string.Join(
                    ";", configuration.GetMetadata("AdditionalPublishProperties"),
                    foundProjectReference.GetMetadata("SetConfiguration"),
                    foundProjectReference.GetMetadata("SetPlatform"),
                    foundProjectReference.GetMetadata("SetTargetFramework"));

                CleanupMetadata(entry, "AdditionalPublishProperties", additionalPublishProperties);

                var publishPropertiesToRemove = string.Join(
                    ";", configuration.GetMetadata("AdditionalPublishPropertiesToRemove"),
                    foundProjectReference.GetMetadata("GlobalPropertiesToRemove"),
                    foundProjectReference.GetMetadata("UndefineProperties"));

                CleanupMetadata(entry, "AdditionalPublishPropertiesToRemove", publishPropertiesToRemove);

                ProjectConfigurations[i] = entry;
            }
        }
        catch (Exception ex)
        {
            Log.LogErrorFromException(ex, showStackTrace: true, showDetail: true, file: null);
        }

        return !Log.HasLoggedErrors;

        static void CleanupMetadata(TaskItem entry, string metadataName, string metadataValue)
        {
            metadataValue = Regex.Replace(metadataValue, ";{2,}", ";");
            metadataValue = metadataValue.Trim(';');
            entry.SetMetadata(metadataName, metadataValue);
        }
    }

    private ITaskItem FindMatchingProject(ITaskItem configuration)
    {
        for (var j = 0; j < ProjectReferences.Length; j++)
        {
            var projectReference = ProjectReferences[j];
            var referenceMetadata = projectReference.GetMetadata("MSBuildSourceProjectFile");
            // All project references should define MSBuildSourceProjectFile but in the ASP.NET Core some special (malformed) references do not.
            // We can be more lenient here and fallback to the project reference ItemSpec if not present.
            referenceMetadata = !string.IsNullOrEmpty(referenceMetadata) ? referenceMetadata : projectReference.ItemSpec;
            var configurationFullPath = configuration.GetMetadata("FullPath");
            var projectReferenceFullPath = Path.GetFullPath(referenceMetadata);
            var matchPath = string.Equals(
                configurationFullPath,
                projectReferenceFullPath,
                OSPath.PathComparison);

            if (matchPath)
            {
                Log.LogMessage(MessageImportance.Low, "Found project reference '{0}' for configuration item '{1}'.", configurationFullPath, projectReferenceFullPath);
                return projectReference;
            }
            else
            {
                Log.LogMessage(MessageImportance.Low, "Rejected project reference '{0}' for configuration item '{1}' because paths don't match.", configurationFullPath, projectReferenceFullPath);
            }
        }

        return null;
    }
}
